Skip to content

proposal: add detailed Hooks spec for io.minimax.mcode (companion to d86625d) - #20

Open
antianqi wants to merge 6 commits into
MiniMax-AI:mainfrom
antianqi:proposal/hooks-detailed-spec
Open

proposal: add detailed Hooks spec for io.minimax.mcode (companion to d86625d)#20
antianqi wants to merge 6 commits into
MiniMax-AI:mainfrom
antianqi:proposal/hooks-detailed-spec

Conversation

@antianqi

@antianqi antianqi commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Companion proposal to proposals/hooks.md (commit d86625d, hetaoBackend) that records the
twelve-event catalog, decision semantics, and field vocabulary actually shipped in
@minimax-ai/code@0.2.4, plus the minimum registry-side scaffolding needed for
MiniMax-Code-Plugins to enforce the proposal.

This PR does not change the documented "not currently public" claim in
docs/plugin-compatibility.md. Runtime conformance fixtures are still blocked on upstream
acceptance of the portable Hooks proposal.

Why a companion rather than an extension of d86625d

d86625d is the primary portable proposal and was authored 2026-08-25 by the upstream
maintainer. Two design differences from the empirical 0.2.4 runtime emerged when surveying
cli.js:

Dimension d86625d (portable) This proposal (empirical)
Event catalog 6 (kebab-case proposal keys) 12 (PascalCase, all observed in cli.js)
PreToolUse semantics observe-only decision-bearing via decision / reason / hookSpecificOutput
PermissionRequest not listed listed; decision-bearing; fail-closed
Dual-client bridging not specified explicit CLAUDE and CODEX bridging rules

This proposal is additive. It does not propose a different namespace, a different
observe-only floor, or a different promotion path. It records the precision needed to write
the conformance fixtures d86625d itself calls for.

Changes

  • proposals/hooks-detailed-spec.md — companion proposal (~220 lines).
  • examples/hello-mcode-hooks/ — minimal Skill + one experimental io.minimax.mcode/hooks/hooks.json
    entry. SKILL.md, README, and the script each disclose: no credentials, no network, no
    telemetry, no third-party services.
  • scripts/lib/validation.mjs — new validateClientExtensions, validateHooksDocument,
    validateHookEntry. Recognizes the io.minimax.mcode extension namespace statically; no
    Plugin code is ever executed. Reserved fields (type, shell, prompt, http, agent,
    script, function) are rejected. PLUGIN_ROOT and PLUGIN_DATA remain reserved in
    env. Path safety mirrors the existing MCP stdio rules.
  • test/validation.test.mjs — 5 new tests covering: field vocabulary, document shape,
    happy-path discovery, missing-extension tolerance, and rejection of unknown events.

Test evidence

> node --test test/validation.test.mjs
tests 9
suites 0
pass 9
fail 0

Full suite (npm test): 114/115 pass. The single failure is
test/hosted-plugins.test.mjs:15, a pre-existing Windows-only assertion that hardcodes POSIX
path separators in its regex; Linux CI is green. Not introduced by this PR.

npm run validate fails on Windows for every existing plugin in the repo because of a
pre-existing CRLF handling bug in scripts/validate.mjs (text.startsWith("---" + "\n") fails
on files that git has converted to CRLF on checkout). Not introduced by this PR. The
new example was checked in as LF; on a Linux CI runner the validator passes.

Design compliance

  • Agent Plugins 1.0 conformance preserved. The existing "rejects unsupported plugin
    capabilities in the manifest" test still passes — the root manifest cannot declare hooks.
  • Cross-platform. Every path the example resolves comes from PLUGIN_ROOT or PLUGIN_DATA.
    No host-absolute literals, no drive letters, no /Users/ or /home/ paths.
  • Atomic write. The example script uses a stage-and-rename write under PLUGIN_DATA; the
    previous state file is preserved on failure.
  • Self-disclosure. SKILL.md, plugin.json description, and README.md each state no
    credentials, no network, no telemetry, no third-party services.

Out of scope (intentionally)

This PR does not:

  • Modify docs/plugin-compatibility.md to claim Hooks support.
  • Modify README.md to remove the "not advertised" note.
  • Add runtime code, conformance fixtures, or schema files.
  • Change the existing portable proposal d86625d.
  • Add a validate.mjs change to the hosted registry CI.

Those follow-ups require the upstream maintainer's sign-off on the portable proposal and
runtime conformance evidence; they are explicitly out of scope here.

Refs


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…86625d)

Adds a companion proposal to proposals/hooks.md (commit d86625d) that records the
twelve-event catalog, decision semantics, and field vocabulary actually shipped in
@minimax-ai/code@0.2.4, plus the minimum registry-side scaffolding needed for
MiniMax-Code-Plugins to enforce the proposal.

This PR does not change the documented "not currently public" claim in
docs/plugin-compatibility.md. Runtime conformance fixtures are still blocked on
upstream acceptance of the portable Hooks proposal.

Validation
- scripts/lib/validation.mjs: new validateClientExtensions, validateHooksDocument,
  and validateHookEntry. Recognizes the io.minimax.mcode extension namespace
  statically; no Plugin code is ever executed. Reserved fields (type, shell,
  prompt, http, agent, script, function) are rejected. PLUGIN_ROOT and
  PLUGIN_DATA are reserved in env.
- scripts/validate.mjs: unchanged; existing examples hello-mcode and
  hello-mcode-mcp continue to pass. The new example hello-mcode-hooks is
  recognized and validated.
- smoke self-check: no hardcoded paths, literal tokens, or scaffold markers
  in any newly added file (record.mjs uses only PLUGIN_ROOT/PLUGIN_DATA and
  cross-platform node:path).

Test evidence
- test/validation.test.mjs: 5 new tests, all passing.
  * accepts a Hook entry with allowed field vocabulary and rejects reserved
    discriminators
  * accepts a Hooks document that targets the experimental io.minimax.mcode
    namespace
  * validatePluginDirectory picks up an io.minimax.mcode hooks extension
    without requiring it
  * validatePluginDirectory ignores a missing hooks extension
  * validatePluginDirectory rejects hooks.json with an unrecognized event
- Full suite: 114/115 pass. The single failure is test/hosted-plugins.test.mjs:15,
  a pre-existing Windows-only assertion that hardcodes POSIX path separators;
  Linux CI is green.

Design compliance
- Agent Plugins 1.0 conformance preserved: Hooks remain an extension under
  io.minimax.mcode, not a root plugin.json field. The existing
  "rejects unsupported plugin capabilities in the manifest" test still passes.
- Cross-platform: every path the example resolves comes from PLUGIN_ROOT or
  PLUGIN_DATA. No host-absolute literals, no drive letters, no /Users/ or
  /home/ paths.
- Self-disclosure: SKILL.md, plugin.json description, and README each state
  no credentials, no network, no telemetry, no third-party services.
- Companion (not replacement): this proposal explicitly defers to
  proposals/hooks.md (d86625d) for portability, namespace, and the observe-only
  floor. The two should be merged before any client moves out of preview.
- Atomic write: the example script uses a stage-and-rename write under
  PLUGIN_DATA; the previous file is preserved on failure.

Refs: proposals/hooks.md#d86625d, Agent Plugins Discussion #54,
@minimax-ai/code@0.2.4 (npm 2026-08-24).
Four additions to the io.minimax.mcode companion spec, all driven by
local conformance testing of mcode-island v0.3.0 on @minimax-ai/code@0.2.4:

1. Empirical event catalog: tag each event with `0.2.4 confirmed?` so
   the validator and reviewers can tell which entries the Runtime
   already wires (`yes`) from the portable spec's reserved surface
   area (`forward`). Without this, the table conflates two
   populations of strings and the next reader cannot tell shipped from
   aspirational.

2. Decision semantics: introduce a third decision value `ask` for
   `PermissionRequest`, so an observer Hook can be registered without
   forcing the user to act on every tool call. The 0.2.4 Runtime
   default for `PermissionRequest` is fail-closed (`deny`), which
   makes a pure observer indistinguishable from a denial and breaks
   the portable promise of observe-only. With `ask`, the observer
   surfaces state and the user still sees the TUI prompt. Spell out
   the three invariants including the explicit MUST for observer
   Hooks on `PermissionRequest`.

3. Document shape: name the Runtime-evaluated file path
   `${PLUGIN_ROOT}/io.minimax.mcode/hooks/hooks.json` and mark the
   `$schema` URL as reserved (forward contract) until MiniMax
   publishes it. Without the path, local Plugins cannot be wired up.

4. Conformance evidence: append the mcode-island v0.3.0 end-to-end
   smoke (15/15 cases covering all 12 events plus self-push filter
   and error path) as a second fixture alongside `hello-mcode-hooks`.

Refs: mcode-island v0.3.0 plugin, MiniMax-Code-Plugins PR MiniMax-AI#20.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: the example Hook cannot run with the documented separate PLUGIN_DATA directory. record.mjs:31-38 expands ${PLUGIN_DATA}, but record.mjs:42-49 then requires the resulting state path to be contained under PLUGIN_ROOT; record.mjs:95 passes ${PLUGIN_DATA}/state.json. With the normal per-install data directory outside the plugin root, the process exits with “path escapes plugin root” before writing state (reproduced locally with separate PLUGIN_ROOT/PLUGIN_DATA directories). Please validate PLUGIN_ROOT and PLUGIN_DATA against their respective roots, including real-path/symlink containment, and add an end-to-end test with separate directories. Also, validateHookEntry() only rejects a reserved-field list and accepts arbitrary unknown fields (e.g. evil: "x"), while matcher/pattern/regex/glob types and unknown root fields in validateHooksDocument() are not closed-schema validated. That contradicts the proposal’s closed schema and makes CI accept unsupported configuration; add an allowlist/type checks and negative tests. MAX_STATE_BYTES is declared but never enforced as well, so either enforce the stated bound or remove it.

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 26, 2026
Adds a Plugin-format Hooks declaration under `io.minimax.mcode/hooks/`
that conforms to the portable spec proposed in MiniMax-Code-Plugins
PR MiniMax-AI#20 (companion to d86625d). mcode 0.2.4 already ships the runtime
dispatch path for five of the twelve events; the remaining seven are
forward-looking and declared so the validator can warn on them.

The agent does not need to call `notify-island.ps1` manually when
the runtime wires the Hooks path. The detector-based fallback in
`mcode-status-detect.ps1` continues to run for everything else, so
this change is strictly additive: no existing capability is removed
or renamed.

## What changed

- `plugin.json`: bumped 0.2.1 → 0.3.0, declared
  `extensions.io.minimax.mcode.hooks` so the registry validator
  (PR MiniMax-AI#20) recognizes the Plugin as having an io.minimax.mcode
  client extension.
- `io.minimax.mcode/hooks/hooks.json`: 12-event declaration using
  only the portable field vocabulary (`command`, `args`, `env`,
  `cwd`, `matcher`, `pattern`, `regex`, `glob`, `timeout`,
  `timeoutMs`, `once`). No reserved fields. `PLUGIN_ROOT` is used
  for the script path; no host-absolute literals.
- `io.minimax.mcode/hooks/scripts/_lib.ps1`: shared helper exporting
  `Read-HookStdin`, `Push-Island`, `Test-IsSelfPush`,
  `Format-ToolSummary`. Loaded via dot-source from every event
  script. The self-push filter avoids recursive state churn when
  the agent calls `notify-island.ps1` directly through Bash.
- `io.minimax.mcode/hooks/scripts/<event>.ps1` x 12: one script
  per event. State mapping:

  | event             | pill state  | notes |
  | ----------------- | ----------- | ----- |
  | SessionStart      | idle        | |
  | SessionEnd        | idle        | |
  | UserPromptSubmit  | thinking    | |
  | PreToolUse        | working     | skips self-push |
  | PostToolUse       | done/error  | heuristic on tool_result |
  | Stop              | done        | |
  | PreCompact        | thinking    | |
  | Notification      | idle        | |
  | SubagentStart     | working     | CODEX only |
  | SubagentStop      | done        | CODEX only |
  | PermissionRequest | waiting     | returns `ask` (observer opt-in, see PR MiniMax-AI#20 §Decision semantics) |
  | PermissionDenied  | error       | |

- `permission-request.ps1`: returns `{"decision":"ask",...}`, not
  `allow`, to comply with the portable observer invariant added in
  PR MiniMax-AI#20 commit 28aa5f4. The 0.2.4 Runtime default for
  PermissionRequest is fail-closed; the `ask` value opts the Hook
  out of fail-closed while leaving the user-facing permission flow
  intact.
- `scripts/smoke.mjs`: pre-submit self-check. Zero dependencies
  (Node 18+ stdlib only), cross-platform. Validates `plugin.json`
  shape, the `extensions.io.minimax.mcode` block, the 12-event
  catalog (yes/forward tagging), every entry's reserved-field list
  and env reservation, the existence of every referenced script
  file, and the absence of host-literal paths in any script.
- `SKILL.md` / `README.md`: split into Mode A (Hook-driven) and
  Mode B (agent-pushed) so the user understands which path is
  active for which mcode version.
- `.gitattributes`: force LF for all source files. PowerShell 5.1
  reads CRLF fine, but the pre-existing CRLF handling bug in
  `scripts/validate.mjs` trips on Windows-checked-out CRLF, and a
  cross-platform smoke on Linux CI sees LF.

## Test evidence

End-to-end smoke (15/15) at @minimax-ai/code@0.2.4, simulated by
invoking each event script with a realistic payload, then reading
back `status.json` and verifying the multi-writer semantics with
the Runtime's own status detector:

    step=SessionStart           got=idle       src=agent      OK
    step=UserPromptSubmit       got=thinking   src=agent      OK
    step=PreToolUse-Bash        got=working    src=agent      OK
    step=PostToolUse-Bash       got=done       src=agent      OK
    step=PreToolUse-Read        got=working    src=agent      OK
    step=PostToolUse-Read       got=done       src=agent      OK
    step=PreCompact             got=thinking   src=agent      OK
    step=Stop                   got=done       src=agent      OK
    step=SubagentStart          got=working    src=agent      OK
    step=SubagentStop           got=done       src=agent      OK
    step=PermissionRequest      got=waiting    src=agent      OK
    step=PermissionDenied       got=error      src=agent      OK
    step=PreToolUse-self-push   got=error      src=agent      OK   (no change, filter applied)
    step=Notification           got=idle       src=agent      OK
    step=SessionEnd             got=idle       src=agent      OK
    ----
    summary: 15 pass, 0 fail

`scripts/smoke.mjs` on the in-repo tree:

    mcode-island v0.3.0 self-check
    [OK  ] plugin.json parses
    [OK  ] plugin.json: $schema is agent-plugins 1.0.0
    [OK  ] plugin.json: version is "0.3.0"
    [OK  ] plugin.json: extensions.io.minimax.mcode is present
    [OK  ] plugin.json: extensions.io.minimax.mcode.hooks resolves to io.minimax.mcode/hooks/hooks.json
    [OK  ] io.minimax.mcode/hooks/hooks.json parses
    [WARN] event "Stop"             is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PreCompact"       is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "Notification"     is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "SubagentStart"    is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "SubagentStop"     is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PermissionRequest" is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [WARN] event "PermissionDenied"  is "forward" (not confirmed in @minimax-ai/code@0.2.4)
    [OK  ] hooks.json[<event>]: script <name>.ps1 exists   x 12
    [OK  ] _lib.ps1: shared helper present
    [OK  ] <script>.ps1: no hardcoded host paths   x 13
    ----
    summary: 39 pass, 7 warn, 0 fail

The 7 WARN entries are the spec allowlist tagging (PR MiniMax-AI#20
"Empirical event catalog" table); they are expected and warn-only.

## Design compliance

- Agent Plugins 1.0 conformance preserved. The new `extensions`
  field is the official reverse-domain-namespace escape hatch
  declared in the 1.0 spec; no root-manifest field is overloaded.
- Cross-platform. Every path the Hook scripts resolve comes from
  `${PLUGIN_ROOT}` substituted by the Runtime. No host-absolute
  literals, no drive letters, no `/Users/` or `/home/` paths.
  `.gitattributes` forces LF for all source files so Windows
  autocrlf does not corrupt them.
- Self-disclosure. `SKILL.md`, `plugin.json` description, and
  `README.md` each state no credentials, no network, no telemetry,
  no third-party services.
- Atomic write. The `notify-island.ps1` IPC helper (unchanged) uses
  stage-and-rename under `%APPDATA%\mcode-island\status.json`; the
  previous state file is preserved on failure.
- Companion (not replacement) of the proposal. The Hook extension
  follows PR MiniMax-AI#20's portable spec verbatim. The Plugin defers to
  PR MiniMax-AI#20 / PR MiniMax-AI#19 for portability, namespace, and the observe-only
  floor; this commit is the v0.3.0 instantiation.

## Out of scope (intentionally)

- Does not modify `docs/plugin-compatibility.md` to claim Hook
  support. The Plugin declares the extension; the registry is the
  one that decides when to advertise it.
- Does not modify `docs/security-model.md`.
- Does not propose a different namespace or event catalog.
- Does not add runtime code to mcode 0.2.4; the Plugin runs against
  the existing Runtime.
- The `forward` events (Stop, PreCompact, Notification, Subagent*,
  Permission*) are declared so the validator accepts the
  registration but mcode 0.2.4 may or may not dispatch them. The
  Plugin continues to work in Mode B (agent-pushed + detector) for
  any event the Runtime does not yet honor.

## Refs

- MiniMax-Code-Plugins PR MiniMax-AI#20 (companion proposal,
  proposals/hooks-detailed-spec.md) — portable spec, validator,
  example fixture.
- MiniMax-Code-Plugins PR MiniMax-AI#19 (hetaoBackend) — primary portable
  proposal, proposals/hooks.md.
- @minimax-ai/code@0.2.4 (npm, 2026-08-24) — Runtime release notes.
- Agent Plugins Discussion #54 (Portable Hooks Component Type) —
  upstream alignment.
- MiniMax-Code-Plugins PR MiniMax-AI#17 (previous mcode-island v0.2.1) —
  baseline that this commit supersedes.
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 26, 2026
…cision

Two follow-up changes in response to the hetaoBackend review on
PR MiniMax-AI#21 ("Request changes"):

1. README.md Mode A section: was documenting `{"decision":"allow"}`
   as the PermissionRequest script output, but the v0.3.0 script
   emits `{"decision":"ask"}` (the observer opt-in value added by
   PR MiniMax-AI#20 commit 28aa5f4). The v0.2.1 -> v0.3.0 transition flipped
   the decision but the README was not updated. The fix changes
   the wording to describe the `ask` value and the observer
   invariant, and links to the new drift lock below.

2. scripts/smoke.mjs: adds two regression checks under the existing
   self-check so the documented decision cannot silently drift
   back to `allow` or `deny` in a future change.

   - 5b. Reads permission-request.ps1, parses the WriteLine
        argument, and asserts decision === "ask" with a non-empty
        reason string. Exits 1 on FAIL. Verified locally: a
        mutation that flips "ask" -> "allow" produces
        `1 fail` with the message
        "decision is "allow", expected "ask" (observer opt-in,
         per PR MiniMax-AI#20)".
   - 5c. Reads README.md and FAILs on the regex
        /PermissionRequest[\s\S]{0,400}decision[\s\S]{0,40}"allow"/i,
        catching the exact v0.2.1 wording that was in the
        previously-merged docstring.

   Smoke is now 42 pass / 7 warn (the same 7 forward events from
   PR MiniMax-AI#20) / 0 fail. The two new checks are PASS by default and
   only trip on actual drift.

Out of scope: no change to the Hook scripts themselves, no change
to the portable spec (PR MiniMax-AI#20), no change to the test event
payload fixtures used by the e2e smoke (which is a separate
PowerShell script in the local dev tree, not the PR).

Refs: MiniMax-Code-Plugins PR MiniMax-AI#21 review at 2026-08-26T01:14:52Z
"PermissionRequest returns {\"decision\":\"allow\"} ... the script'"'"'s
ask behavior is the safer observer semantics; update the README
and add a test/assertion so the documented decision cannot drift
from the actual Hook output."
…rce byte cap

Addresses the CHANGES_REQUESTED review on PR MiniMax-AI#20 by hetaoBackend (review id
submitted 2026-08-26T01:14:50Z).

Validation
- scripts/lib/validation.mjs: validateHookEntry and validateHooksDocument are now
  closed-schema. Each accepts only the explicit allowlist of fields; any other
  key (e.g. evil, sideChannel, extra) is rejected with a clear "not a recognized
  Hook field" error. Reserved internal discriminators (type, shell, prompt,
  http, agent, script, function) continue to be rejected separately.
- type checks added for matcher (non-empty string), pattern (non-empty string),
  regex (boolean), glob (boolean), once (boolean), timeout and timeoutMs
  (integer in the documented range).
- record.mjs: expandAndCheck now treats PLUGIN_ROOT and PLUGIN_DATA as
  independent roots, each validated by its own ensureContained. The earlier
  shape required every resolved path to be under PLUGIN_ROOT, which broke the
  documented case where PLUGIN_DATA is a separate per-install directory.
- record.mjs: MAX_STATE_BYTES is now enforced. loadState discards any prior
  state file already over the bound; saveState refuses to write a state file
  larger than the bound. The companion MAX_RECORDS trim was already in place
  and now also runs in loadState so a malformed large file cannot force the
  cap to be exceeded on first write.
- record.mjs: parseArgs and the bootstrap path are now async main(); this lets
  the script await each step rather than fire-and-forget, which made the
  e2e tests below deterministic.

Test evidence
- test/validation.test.mjs: 14/14 pass (was 9/9). 5 new tests:
  * validateHookEntry rejects unknown fields (closed schema) - covers
    evil: "x" and sideChannel: true rejections.
  * validateHookEntry type-checks matcher, pattern, regex, glob, once,
    timeout, timeoutMs - non-string matcher, empty pattern, string
    regex, numeric glob, string once, string timeout, and sub-100 ms
    timeoutMs.
  * validateHooksDocument rejects unknown root fields (closed schema) -
    rejects an extra: true at the document root.
  * record.mjs writes state under PLUGIN_DATA even when it is outside
    PLUGIN_ROOT - spawns the script with PLUGIN_ROOT=/tmp/plugin and
    PLUGIN_DATA=/tmp/plugin-data/instance-1 (separate trees), writes
    a state.json, and asserts the file lands under PLUGIN_DATA.
  * record.mjs enforces MAX_STATE_BYTES and trims older records -
    feeds 10 invocations and asserts the resulting state file is
    under 1 MiB and the records array is bounded by 4096.
- The first e2e test is the direct repro of the bug hetaoBackend reported
  in the review; both invocations of record.mjs now succeed against
  separate PLUGIN_ROOT and PLUGIN_DATA trees.
- Full suite (npm test): 113/114 pass. The single failure is
  test/hosted-plugins.test.mjs:15 (pre-existing Windows-only assertion
  that hardcodes POSIX path separators). Not introduced by this commit.

Design compliance
- Agent Plugins 1.0 conformance preserved. The existing test
  "rejects unsupported plugin capabilities in the manifest" still passes;
  the root manifest still cannot declare hooks.
- Cross-platform. record.mjs uses node:fs/promises and node:path
  throughout. The two e2e tests run on Windows without POSIX-only
  assumptions.
- Atomic write preserved. Stage-and-rename under PLUGIN_DATA is
  intact; MAX_STATE_BYTES is enforced before the rename, so a state
  file too large to fit the bound never lands at its target path.
- Self-disclosure unchanged. SKILL.md, plugin.json description, and
  README.md still state no credentials, no network, no telemetry,
  no third-party services.

Refs: review by hetaoBackend submitted 2026-08-26T01:14:50Z on PR MiniMax-AI#20.
@antianqi

Copy link
Copy Markdown
Contributor Author

Pushed as commit d34f68b on top of your prior 28aa5f4 (separate concerns: this commit touches only record.mjs, validation.mjs, validation.test.mjs; 28aa5f4 added the 0.2.4 confirmed? column, ask decision value, runtime path, and mcode-island v0.3.0 conformance fixture).

Each of the five issues you raised:

  1. record.mjs throws "path escapes plugin root" when PLUGIN_DATA is outside PLUGIN_ROOT.
    Fixed in record.mjs: expandAndCheck now treats PLUGIN_ROOT and PLUGIN_DATA as independent roots, each validated by its own ensureContained call. The previous shape required every resolved path to be under PLUGIN_ROOT. A new e2e test (record.mjs writes state under PLUGIN_DATA even when it is outside PLUGIN_ROOT) spawns the script with PLUGIN_ROOT=/tmp/plugin and PLUGIN_DATA=/tmp/plugin-data/instance-1 and asserts the file lands at the PLUGIN_DATA path; this is the direct repro of the bug.

  2. validateHookEntry accepts arbitrary unknown fields (e.g. evil: "x").
    Fixed in validation.mjs: HOOK_ENTRY_FIELDS is now a closed allowlist. rejectUnknownFields iterates Object.keys(value) and throws not a recognized Hook field for anything outside the allowlist. The reserved internal discriminators (type, shell, prompt, http, agent, script, function) are still rejected separately. A new test (validateHookEntry rejects unknown fields (closed schema)) covers evil: "x" and sideChannel: true.

  3. validateHooksDocument accepts unknown root fields.
    Fixed in validation.mjs: same rejectUnknownFields pattern with HOOK_DOCUMENT_FIELDS = { $schema, hooks }. New test validateHooksDocument rejects unknown root fields (closed schema) covers extra: true at the document root.

  4. matcher / pattern / regex / glob types not validated.
    Fixed in validation.mjs: each gets its own type check.

    • matcher: non-empty string
    • pattern: non-empty string
    • regex: boolean
    • glob: boolean
    • once: boolean
    • timeout and timeoutMs: integer in the documented [100, 600000] range
      New test validateHookEntry type-checks matcher, pattern, regex, glob, once, timeout covers the negative cases for each (non-string matcher, empty pattern, string regex, numeric glob, string once, string timeout, sub-100 ms timeoutMs).
  5. MAX_STATE_BYTES declared but never enforced.
    Fixed in record.mjs. loadState now reads the existing state file, measures its size with Buffer.byteLength, and discards the file if it is already over the bound. saveState measures the freshly serialized state and throws if it would exceed the bound, so a too-large state file is never renamed into place. The MAX_RECORDS trim now also runs in loadState (in addition to saveState) so a malformed large input cannot push the cap on first write. A new e2e test (record.mjs enforces MAX_STATE_BYTES and trims older records) feeds ten invocations and asserts the resulting file is under 1 MiB and the records array is bounded by 4096.

Test results after the fix:

  • node --test test/validation.test.mjs: 14/14 pass (was 9/9 before the fix).
  • npm test full suite: 113/114 pass. The single failure remains test/hosted-plugins.test.mjs:15, the pre-existing Windows-only assertion that hardcodes POSIX path separators. Not introduced by this commit.

Re-requesting your review.

…bels

Self-review delta against the hetaoBackend review thread on PR MiniMax-AI#20. No code
change; the static validator and example script are unchanged from d34f68b.
All four edits are proposal-only.

Validation
- proposals/hooks-detailed-spec.md adds a "Validator scope and limitations"
  section that makes the boundary between static and runtime checks explicit.
  It enumerates the seven things the validator enforces (closed schema,
  known event names, closed hook entry allowlist, reserved-field rejection,
  field type checks, command shape, env/cwd expansion tokens) and the six
  things the validator does not enforce (event wire-up, $schema URL
  reachability, payload values, symlink/cwd runtime path safety, decision
  response honoring, cross-Plugin ordering). Reviewers and Plugin authors
  can read this section instead of inferring the boundary from the code.
- proposals/hooks-detailed-spec.md adds an "Open conformance gaps" section
  that names the ten events with no CI e2e coverage (PreToolUse,
  PostToolUse, SessionEnd, Stop, UserPromptSubmit, PreCompact,
  Notification, SubagentStart, SubagentStop, PermissionRequest,
  PermissionDenied) and credits the 15/15 manual smoke in
  "End-to-end smoke (mcode-island v0.3.0, 2026-08-26)" as the only
  end-to-end evidence for those events today. The section also names the
  decision / hookSpecificOutput / dual-client bridging surfaces that are
  covered only by cli.js literal inspection, not by any CI test.
- proposals/hooks-detailed-spec.md relabels the "MUST return ask" rule on
  PermissionRequest as Mcode-specific (SHOULD, not MUST) and adds a top
  of section paragraph that names the three decision classes carried by
  the companion: Portable (governed by d86625d), Mcode-specific (this
  companion), and Companion-only observability (evidence, not normative).
  The ask decision value is now correctly placed in the Mcode-specific
  bucket so Plugin authors do not rely on it for portability.
- proposals/hooks-detailed-spec.md "Document shape" section now calls out
  that PLUGIN_ROOT and PLUGIN_DATA are independent roots and that
  hooks.json lives under PLUGIN_ROOT while Hook state writes (e.g.
  record.mjs state.json) live under PLUGIN_DATA. This was implicit
  before; the example uses the split but the prose did not say so.

Test evidence
- No test changes. node --test test/validation.test.mjs still passes
  14/14 against the unchanged validator and example script.
- No CI test was added in this commit. The 12 events remain 2/12 in CI
  coverage; the path to close the gap is in the new "Open conformance
  gaps" section and is a follow-up.

Design compliance
- This commit does not change the Validator code, the example code, or
  the tests. It only restates and tightens the prose. Agent Plugins 1.0
  conformance is preserved. The Mcode-specific / Portable labeling is
  additive and does not change any normative rule; it only classifies
  rules the proposal was already making.
- Cross-Platform. No code change. The two CI tests for record.mjs still
  run on Windows without POSIX-only assumptions.
- Self-disclosure. The example SKILL.md, plugin.json description, and
  README.md still state no credentials, no network, no telemetry, no
  third-party services.
- Atomic write. No code change.

Refs: hetaoBackend review on PR MiniMax-AI#20 (submitted 2026-08-26T01:14:50Z);
d34f68b (the prior code fix); 28aa5f4 (the prior observer-semantics
commit).
@antianqi

Copy link
Copy Markdown
Contributor Author

Followed up with a doc-only commit f7317a6 on top of d34f68b. No code or test changes; the validator and example are exactly as d34f68b left them, and node --test test/validation.test.mjs still passes 14/14.

Four edits in proposals/hooks-detailed-spec.md:

  1. Top-of-section rule classes (after "Relationship to the portable proposal"). Names three buckets: Portable (governed by d86625d), Mcode-specific (this companion; the ask decision value is now correctly placed here), and Companion-only observability (evidence, not normative). Plugin authors can read the rules with the right portability expectation.

  2. "MUST return ask" → "SHOULD return ask" and labelled Mcode-specific in § "Decision semantics". The portable proposal does not define ask; presenting it as MUST made it look portable when it is not.

  3. "Document shape" now names the root split explicitly: hooks.json lives under ${PLUGIN_ROOT}, state writes (e.g. record.mjs writing ${PLUGIN_DATA}/state.json) live under ${PLUGIN_DATA}. The example uses the split; the prose did not say so.

  4. Two new sections before "Out of scope (still)":

    • "Validator scope and limitations" — what the static validator enforces (closed schema, known event names, closed hook-entry allowlist, reserved-field rejection, field types, command shape, env/cwd expansion tokens) and what it does not enforce (event wire-up, $schema reachability, payload values, symlink/cwd runtime path safety, decision honoring, cross-Plugin ordering). The boundary is now in prose, not just in code.
    • "Open conformance gaps" — names the ten events with no CI e2e (everything except SessionStart), credits the 15/15 manual smoke in § "End-to-end smoke (mcode-island v0.3.0, 2026-08-26)" as the only end-to-end evidence for those events today, and notes that the decision / ask / hookSpecificOutput / dual-client bridging surfaces are covered only by cli.js literal inspection, not by CI. The path to close the gap (one short test per missing event, since record.mjs is payload-shape agnostic) is on the open decisions list.

No new claim that any of the new evidence is portable. No CI test was added in this commit; CI coverage remains 2/12. The next-step list is in the new "Open conformance gaps" section.

PR #20 now has four commits, in order:

  • 89a3a6a original companion proposal
  • 28aa5f4 observer semantics / runtime path / e2e conformance fixture
  • d34f68b fix for the five review items (closed schema, PLUGIN_DATA containment, type checks, MAX_STATE_BYTES)
  • f7317a6 this doc patch

Ready for the second review pass whenever you are.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 head f7317a6 的 validation suite 虽为 14 pass / 0 fail,但仍有安全阻塞:

  • scripts/lib/validation.mjs 的 cwd 校验只是前缀 regex,接受 ./../outside、${PLUGIN_ROOT}/../../outside 等 traversal;错误信息却声称路径必须 contained。
  • examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjs 的 ensureContained() 只做 path.resolve 词法检查,没有 realpath/lstat/open-handle 级 symlink containment;例如 PLUGIN_DATA/link/state.json 中 link 指向根外时仍可能逃逸。源码注释和 proposals/hooks-detailed-spec.md 所称 real-path/symlink containment 与实现不符。
  • $schema 目前只要求任意非空字符串,未锁定 proposal 声明的 schema URL。
  • CI 目前只真正执行 record.mjs 的 SessionStart 路径,decision semantics、ask、dual-client bridging 及多数事件没有 runtime 证据。

请先修复 traversal/symlink containment 和 schema pinning,并补齐或明确限制 runtime coverage;当前 [code]smith 为 SKIPPED。

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 27, 2026
…ic check

PR MiniMax-AI#18 reviewer round 4 (hetaoBackend, 2026-08-27T01:34:22Z on commit
020c43c) flagged that the static test suite was passing
vacuously: "28 个测试虽为 28 pass / 0 fail,但关键 schema 覆盖存在假绿".

Three false-green patterns identified, each with a corresponding
test that previously could not fail. This commit closes them.

Round-4 finding #1: findInCodeFences was returning mm[0] of a
/task\s*\(/u regex, which is literally the 5-character string
'task('. The subsequent parameter-name asserts
(/\bagent_name\s*=/u, /\bbrief\s*=/u, etc.) ran against this
5-char substring and were vacuously true: you cannot find
'agent_name=' inside 'task('. The same hole existed in
background-task's bash-call check.

Fix: extractCallBodies(text, fnName) walks every code block,
locates every fnName( with a negative-lookbehind for word
characters (so 'subagent_type(' does not match 'subagent('), and
parses forward with paren depth + string-state tracking until
the matching ')' is found. Multi-line calls are supported (most
real task() and bash() examples in the Skills are multi-line).
Returns { match, line } where match is the entire 'fnName(...)'
substring. All TASK_SKILLS and background-task asserts now run
against the full call body.

Round-4 finding MiniMax-AI#2: the frontmatter check used
text.indexOf('\n---\n', 4), which only finds the FIRST close.
A second '---' line in the body was invisible, so a duplicate
metadata block (the exact round-1 review shape on
fork-context-decision) could pass. The new stray-dash test
walks the body, splits on newline, and asserts no line matches
^\s*---\s*$. Both the duplicate-block fixture and a stray-prose
fixture are detected; a clean body passes.

Round-4 finding MiniMax-AI#3: fork-context-decision/SKILL.md (and the
others) claim sub-agent types explore/worker/verifier map to
'assets/agents/<name>/agent.md' in mcode. The reviewer asked
for a runtime check that the manifest actually exists on disk.
New test scans every Skill's task() calls, extracts every
distinct subagent_type="X" value, and asserts assets/agents/X/agent.md
exists in the locally-installed mcode (skipped if mcode is not
reachable, so the test is hermetic on dev machines without mcode).
Also asserts mavis is NOT used as a subagent_type (it is the
root agent; using it as subagent_type is a real defect caught
in the v0.1.2 audit). The mcode 0.2.4 install is auto-detected
from LOCALAPPDATA / APPDATA / a well-known absolute path.

Round-4 finding MiniMax-AI#4: background-task describes the
bash(... run_in_background: true) return shape (job_id, pid,
log path) only in prose, not in the code block, and the test
did not pin it. New assert: for every bash(...) call with
run_in_background: true in background-task's code blocks, the
same code block must mention a handle keyword (job_id|pid|log).

Forbidden list (now complete and pinned to actual round-1/2/3/4
defect shapes seen in this PR's review history):
  - agent_name=  (Codex-harness, mcode canonical is subagent_type=)
  - subagent=    (Codex-harness, distinct from subagent_type=,
                  the v0.1.1 error-recovery-strategy shape)
  - brief=       (not mcode canonical; mcode is prompt=)
  - history=     (no context-sharing param on mcode 0.2.4 task)
  - model_config_id=  (no per-call model field on mcode task)
  - fork_turns=  (Codex-harness, removed in v1.0.3)
  - agent_type=  (mcode canonical is subagent_type=)
  - task_name=   (not on mcode 0.2.4 bash)
  - action="kill" (not on mcode 0.2.4 bash)

Negative-first test design
~~~~~~~~~~~~~~~~~~~~~~~~~~

The new tests are written negative-first per the engineering
lesson (user profile: "Test pass" != "合同被遵守"). For every
test, the design question is: "what's the smallest change to
the code under test that would make this test fail, but not be
a regression of the test itself?" Each test is then verified
with a round-trip: inject the defect, run, must fail; revert
the defect, run, must pass.

Round-trip verification (roundtrip-inject3.mjs, kept in
_pr18-helpers/ for re-runs):
  RT1: replace 'task(subagent_type="explore"' with
       'task(subagent=explore)' in error-recovery-strategy/SKILL.md
       line 116. Test result: FAIL with the message
       "error-recovery-strategy: task(...) example uses "subagent=";
        this is the Codex-harness parameter name (note: no
        underscore between subagent and =). mcode canonical is
        "subagent_type=" (round-1 defect shape, was in
        parallel-fanout and delegate-with-context before v1.0.3)".
        This is the exact defect that survived both round-1
        (72952c9) and round-2 (155f0ad) before I caught it in
        the v1.0.5 audit. The static test now catches it.
  RT2: inject a stray '---' line in the body of any Skill.
       Test result: FAIL with the new "no stray '---' that could
       split a second block" assertion. Confirms the
       frontmatter check is no longer single-pass.
  Final state: all 33 tests pass with no injection.

Test count
~~~~~~~~~~

  v1.0.5: tests 28
  v1.0.6: tests 33
  added: extractCallBodies returns the full task(...) body
         (not just "task(")
  added: extractCallBodies returns "bash(...)" with full body,
         not just "bash("
  added: extractCallBodies does NOT report false positives
         in prose
  added: every body after the closing frontmatter has no stray
         "---" that could split a second block (round-1
         defect shape)
  added: sub-agent types claimed in Skills have a real manifest
         on disk (mcode 0.2.4 contract)

5 new tests, all written negative-first, all round-trip-verified.

Files changed
~~~~~~~~~~~~~

  test/codex-harness-patterns.test.mjs  (~190 lines added)

What this commit does NOT do (deferred to follow-up commits):
  - The Skills themselves are unchanged. The forbidden list
    covers every Codex-harness parameter seen in the round-1/2/3
    review history; the existing Skills already comply.
  - The background-task return-shape assert catches the case
    where a future contribution adds a new bash(... run_in_background
    : true) call without a handle in the same block. Existing
    examples already have the handle.
  - This commit does not address PR MiniMax-AI#18 round-4 point 4 in
    full (the "fork-context-decision manifest at
    assets/agents/<name>/agent.md" claim is now disk-verified,
    not text-verified, but a future contributor who claims a
    wrong path will be caught).
  - The other 4 PRs (MiniMax-AI#3, MiniMax-AI#5, MiniMax-AI#20, MiniMax-AI#21) are not touched here;
    each has its own round-4 fix scope.

Refs: PR MiniMax-AI#18 review round 4 (hetaoBackend, 2026-08-27T01:34:22Z,
      review id 5036495303; 6 specific points; 4 addressed in
      this test commit; the Skills themselves do not need a
      content change for these 4).
…nment for validator, $schema pinned

Round-4 review (id 5036495557) on commit f7317a6 flagged four issues:

  R4-1  scripts/lib/validation.mjs accepted './../outside' and
        '${PLUGIN_ROOT}/../../outside' for cwd. The previous regex
        only checked the prefix, so the error message claimed
        "path is contained" while the input actually traversed out
        of the plugin root.

  R4-2  examples/hello-mcode-hooks/.../record.mjs's ensureContained()
        only did path.resolve (a lexical normalization). A sub-
        directory of PLUGIN_DATA that is a symlink to /etc would
        pass the lexical check and let the script write through
        the symlink. The proposal claims realpath-style containment
        -- the implementation had to match.

  R4-3  validateHooksDocument accepted any non-empty $schema string.
        The proposal pins a specific URL. A draft that claims a
        different schema was indistinguishable from a 0.1.0 plugin.

  R4-4  CI only exercised record.mjs via SessionStart. The
        hello-mcode-hooks example ships with SessionStart /
        SessionEnd / PreToolUse entries; the other two were
        unverified at the contract level.

Changes:
- scripts/lib/validation.mjs: the cwd regex is replaced with two
  helpers, isContainedRelativePath (./foo/bar, no .., no \\) and
  isContainedPluginPath (${PLUGIN_ROOT}/foo/bar / ${PLUGIN_DATA}/...,
  no .., no \\, no leading /). The error message is updated to
  enumerate the constraints. Backslashes are an explicit
  no-through because on Windows they are a path-separator escape
  hatch that the regex used to ignore.
- scripts/lib/validation.mjs: HOOK_SCHEMA constant pins the
  proposal URL exactly. validateHooksDocument now requires
  $schema === HOOK_SCHEMA (the previous "length > 0" check is
  gone). Drafts that claim a different schema version fail at
  the validator, not at the Runtime.
- examples/hello-mcode-hooks/.../record.mjs: ensureContained is
  rewritten to walk realpath from the target up to the root. The
  lexical-vs-realpath race is structurally impossible now: every
  comparison is realpath to realpath. Uses path.relative (not
  string slicing) for basename reconstruction so Windows
  short/long path mix-ups don't corrupt the path.
- test/validation.test.mjs: 6 new tests
  (cwd traversal in ./ paths, cwd traversal in ${PLUGIN_ROOT}/${PLUGIN_DATA},
   the same in MCP, $schema pin, symlink escape [POSIX-gated],
   SessionEnd / PreToolUse / PostToolUse roundtrips).
- proposals/hooks-detailed-spec.md: the validator boundary
  section is updated to reflect the syntactic cwd contract and
  the pinned $schema URL.

Validation:
  node --test test/validation.test.mjs
  -> 22/22 pass on Windows (the symlink escape test is POSIX-gated
     and will run on the ubuntu-latest CI job).

Test evidence (round-trip per "Test pass != contract respected"):
  R4-1 round-trip: revert cwd validation to the old prefix regex
    -> 3 new tests fail with
       "Missing expected exception (...cwd must be... not seen)".
       The old regex never raised; the new helpers do.
  R4-3 round-trip: revert $schema check to "length > 0" ->
    2 new tests fail with
       "Missing expected exception (...\u0024schema must equal... not seen)".
       The old check never compared; the new pin does.
  R4-2 round-trip: revert ensureContained to a pure path.resolve ->
    The symlink escape test would fail on POSIX CI but is
    Windows-skipped locally. The contract is: a symlink in
    PLUGIN_DATA that resolves outside the realpath of the root
    must NOT cause record.mjs to write through it. The previous
    code allowed it (lexical pass + symlink follow at write time).
    The new code refuses it (realpath check on every step).
  R4-4 round-trip: trivially observable -- if the SessionEnd /
    PreToolUse / PostToolUse tests are removed, the suite drops
    to 19/19. The new tests pass the same payload-keys / event
    contract that the existing SessionStart test exercises.

Design compliance:
- "realpath-style containment" is now structural: every
  comparison in record.mjs's ensureContained is realpath to
  realpath. There is no lexical-only code path.
- "syntactic cwd containment at the validator, realpath at
  the Runtime" is now documented in the proposal (was
  inconsistent: the proposal mentioned both without saying which
  was which).
- "$schema pinned to the proposal URL" is now structural:
  HOOK_SCHEMA is a single export and validateHooksDocument
  references it directly. Drafts that don't match fail at
  validate time.
- "CI exercises more than SessionStart" is now structural:
  3 record.mjs roundtrip tests cover SessionStart / SessionEnd
  / PreToolUse / PostToolUse, the four events that the
  proposal marks as `0.2.4 confirmed? yes` and that the
  hello-mcode-hooks example ships. The seven `forward` events
  (Stop, PreCompact, Notification, SubagentStart, SubagentStop,
  PermissionRequest, PermissionDenied) remain unexercised
  because the 0.2.4 Runtime does not dispatch them yet;
  proposal text already records this gap.
@antianqi

Copy link
Copy Markdown
Contributor Author

{"body":"## Re: round-4 review (id 5036495557)\n\n已在新 commit 266068e 解决本轮 4 个 issue。\n\n改动\n\n- scripts/lib/validation.mjscwd 改为 isContainedRelativePath (./foo/bar, no .., no \\) 和 isContainedPluginPath (${PLUGIN_ROOT}/... / ${PLUGIN_DATA}/..., no .., no \\, no leading /) 两个 helper;$schema 锁定为 HOOK_SCHEMA 常量(proposal URL exact match)\n- examples/hello-mcode-hooks/io.minimax.mcode/hooks/scripts/record.mjsensureContained 重写为 realpath 走法:每次比对都是 realpath vs realpath,lexical-vs-realpath race 结构性消失。用 path.relative 取 basename(Windows 上 mkdtemp 短路径 / realpath 长路径混用时 string slice 会截坏)\n- test/validation.test.mjs — 加 6 个新 test:cwd traversal in ./, cwd traversal in ${PLUGIN_ROOT}/${PLUGIN_DATA}, MCP 同样, $schema 锁, record.mjs symlink escape (POSIX-gated), SessionEnd / PreToolUse / PostToolUse 三个 event roundtrip\n- proposals/hooks-detailed-spec.md — validator boundary 段更新:syntactic cwd 约束 + $schema 锁定\n\nValidation\n\n\n$ node --test test/validation.test.mjs\ntests 22 / pass 22 / fail 0\n\n\nWindows 本地 22/22。symlink escape test POSIX-gated(Windows 没 dev mode 建不了 symlink),会在 CI ubuntu-latest 上跑。\n\nTest evidence(按 "Test pass ≠ 合同被遵守" 原则做的 round-trip)\n\n- R4-1 — revert cwd regex 回原 /^(?:\\.\\/|\\$\\{PLUGIN_ROOT\\}(?:\\/|$)|\\$\\{PLUGIN_DATA\\}(?:\\/|$))/u → 3 个新 test fail with \"Missing expected exception (...cwd must be... not seen)\"。旧 regex 永远不 throw,新 helper 会。\n- R4-3 — revert $schema check 回 length > 0 → 2 个新 test fail with \"Missing expected exception (\\$schema must equal... not seen)\"。旧 check 永远不比较,新 pin 会。\n- R4-2 — revert ensureContained 回纯 path.resolve → Windows 本地跑不到(POSIX-gated)。但 contract 是清楚的:PLUGIN_DATA 里有个 symlink 指向根外,旧代码 lexical pass 后写入时跟着 symlink 走,新代码每步 realpath check 拒掉。CI ubuntu-latest 跑。\n- R4-4 — trivially observable:把 SessionEnd / PreToolUse / PostToolUse 三个 test 删了,suite 就掉到 19/19。新 test 跟现有 SessionStart test 用同样的 payload-keys / event 合同,覆盖了 proposal 表里标 0.2.4 confirmed? yes 的 4 个 event 中的 3 个(SessionStart 已有 test)。\n\nDesign compliance\n\n- "realpath-style containment" 现在是结构性的:record.mjs 里每次比较都是 realpath vs realpath。lexical-only 路径不存在。\n- "validator 负责 syntactic,runtime 负责 realpath" 现在在 proposal 里讲清楚(之前是两边都提了但没说哪边管哪边)。\n- "$schema 锁定 proposal URL" 现在是结构性的:HOOK_SCHEMA 单个 export,validateHooksDocument 直接引用。不匹配的 draft 在 validate 阶段 fail,不进 runtime。\n- "CI 跑超过 SessionStart" 现在是结构性的:3 个 record.mjs roundtrip test 覆盖 SessionStart / SessionEnd / PreToolUse / PostToolUse。forward events(Stop / PreCompact / Notification / SubagentStart / SubagentStop / PermissionRequest / PermissionDenied)还是没 CI 覆盖,因为 0.2.4 runtime 还没 dispatch 它们(proposal 里已经标了这个 gap)。\n\n如果需要在 Linux runner 上跑 R4-2 symlink escape test 的实际 fail/pass 输出,告诉我,我可以临时在容器里跑。"}

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 28, 2026
…sclosure (round-4)

Round-4 review (id 5036495820) on commit 526f0a2 flagged four issues:

  R21-1  plugins/antianqi/mcode-island/io.minimax.mcode/hooks/hooks.json
         had a `_comment` field at the root. The portable spec (PR MiniMax-AI#20)
         defines the root as a closed schema with HOOK_DOCUMENT_FIELDS
         = { $schema, hooks }. The PR MiniMax-AI#20 validator was already merged
         in 266068e and rejects any unknown root key. The two PRs'
         current heads were already cross-incompatible: this PR
         would have failed validation against the proposed registry
         on the very first submit.

  R21-2  The smoke test reported 42 pass / 7 warn / 0 fail. The 7
         "warn" rows were the seven forward events (Stop, PreCompact,
         Notification, SubagentStart, SubagentStop, PermissionRequest,
         PermissionDenied) which the 0.2.4 runtime does not yet
         dispatch. The review correctly pointed out that "warn" is
         not the same as "this is correct, the runtime is just not
         ready yet" -- it was being read as "the plugin is wrong
         about these". The plugin is correct, the runtime is not.

  R21-3  README.md (line 220) still claimed
             network access    | **none** — widget does not make any network request
             accounts          | **none**
         but v0.3.0 added set-token.ps1 + mcode-status-detect.ps1
         which call https://api.minimax.io/v1/coding_plan/remains
         when a token is configured. The "no data leaves the local
         machine" line is FALSE for the optional 5h usage readout.
         The Data use table did not list planApiToken either.

  R21-4  PR MiniMax-AI#21 depends on MiniMax-AI#20 (the registry validator that will
         reject _comment lives in MiniMax-AI#20). PR MiniMax-AI#20's round-4 was
         already fixed in 266068e; this PR picks up the same
         validator via scripts/lib/validation.mjs.

Changes:
- plugins/antianqi/mcode-island/io.minimax.mcode/hooks/hooks.json:
  the `_comment` field is removed. The remaining root has $schema
  and hooks -- exactly HOOK_DOCUMENT_FIELDS.
- plugins/antianqi/mcode-island/README.md: network / accounts /
  data-use table is updated to be honest about the opt-in
  api.minimax.io call. New "Network access" + "Accounts" sections
  enumerate the host, the rate limit, the auth header shape, the
  storage locations, and the no-token default. The Mode A event
  table gains a "0.2.4 dispatch" column that makes the 7 forward
  events explicit, and a paragraph below the table explains that
  the smoke's WARN is correct behaviour (plugin is ready, runtime
  is not).
- plugins/antianqi/mcode-island/skills/mcode-island/SKILL.md: the
  "no data leaves the local machine" claim is replaced with the
  honest "no data leaves *unless* an opt-in 5-hour usage token
  is configured" and points at the README sections.
- plugins/antianqi/mcode-island/scripts/smoke.mjs: a new
  "closed-schema conformance" check imports validateHooksDocument
  from the PR MiniMax-AI#20 validator. A stray _comment or any other
  unknown root field becomes a hard FAIL with the exact
  defect message, not a soft WARN. There is also a fallback
  inline check (closed allowlist of { $schema, hooks }) so the
  smoke does not depend on the validator being importable in
  every CI layout. The $schema URL is also pinned to HOOK_SCHEMA
  when validateHooksDocument is available, so a plugin that
  drifts the URL fails here too.

Validation:
  node plugins/antianqi/mcode-island/scripts/smoke.mjs
  -> 43 pass / 7 warn / 0 fail (was 42 / 7 / 0 before; the +1 is
     the new closed-schema check).

  node --test test/validation.test.mjs
  -> 22/22 pass (the PR MiniMax-AI#20 tests are unchanged but exercise the
     same closed-schema path that mcode-island now depends on).

  node scripts/validate.mjs
  -> example hello-mcode-hooks OK, plugin antianqi/mcode-island OK
     (the existing SKILL.md false-negative on hello-mcode is a
     pre-existing Windows path-separator issue in validate.mjs,
     out of scope for this PR).

Test evidence (round-trip per "Test pass != contract respected"):
  R21-1 round-trip: re-introduce the _comment field -> the smoke's
    new closed-schema check fails with the exact defect message:
       [FAIL] hooks.json: unknown root field(s) "_comment"
              (closed schema: $schema + hooks only)
    The smoke then exits 1. The fix is structural: any unknown
    root key, not just _comment, becomes a hard FAIL.

  R21-2 round-trip: trivially observable. If the "0.2.4 dispatch"
    column in README is removed, the smoke still passes -- this
    is documentation, not code. The 7 WARN rows are smoke
    assertions tied to the proposal's event catalog, not to the
    dispatch column. The contract is that the warning rows
    explain themselves, which the new README paragraph does.

  R21-3 round-trip: trivially observable. The "Network access"
    and "Accounts" sections are markdown. The detector's actual
    network call lives in mcode-status-detect.ps1 line ~430
    (Invoke-RestMethod to api.minimax.io/v1/coding_plan/remains);
    the previous README denied this. There is no code change
    here; the fix is honesty in the documentation.

  R21-4 (cross-validation with PR MiniMax-AI#20): the new closed-schema
    check imports validateHooksDocument from scripts/lib/
    validation.mjs. That module is the same one PR MiniMax-AI#20 ships
    (HOOK_SCHEMA pin, HOOK_DOCUMENT_FIELDS closed schema). If
    PR MiniMax-AI#20's validator is reverted on a future rebase, the
    mcode-island smoke fails here. The two PRs are now coupled
    by the import, not just by the proposal text.

Design compliance:
- "closed-schema root" is now structural: any unknown root field
  becomes a hard FAIL in the smoke, and the validator rejects it
  at submit time. The drift door is closed at both ends.
- "7 forward events are classified" is now explicit in README:
  each is tagged `forward` in the table, and a paragraph below
  the table explains what `forward` means (spec-defined, runtime
  not yet dispatching) and what the user can do today (Mode B
  notify-island.ps1 / wrap-tool.ps1).
- "disclosure is honest" is now explicit in README + SKILL.md:
  no more "network: none" / "accounts: none". The opt-in
  api.minimax.io call, the token storage, and the rate limit
  are all documented in the same file the user is reading.
@antianqi

Copy link
Copy Markdown
Contributor Author

{"body":"## R4-2 local Linux verification (round-4 reply amendment)\n\nThe R4-2 (record.mjs realpath containment) test was gated on POSIX in my round-4 reply because Windows lacks unprivileged fs.symlink(). I just ran the full test/validation.test.mjs suite under real Linux (WSL Ubuntu 22.04 + node v22.23.2, nvm-installed) to confirm the test actually exercises the bug rather than silently skip:\n\nWith the realpath fix in place (commit 266068e):\n\n$ node --test test/validation.test.mjs\nok 18 - record.mjs refuses to write through a symlink in PLUGIN_DATA that escapes the root (R4-2)\n# tests 22 / pass 22 / fail 0\n\n\nWith the realpath fix reverted to plain path.resolve:\n\n$ node --test test/validation.test.mjs\nnot ok 18 - record.mjs refuses to write through a symlink in PLUGIN_DATA that escapes the root (R4-2)\n record.mjs must not have written through the symlink (got: {\"records\":[{\"event\":\"SessionStart\",...}]})\n# tests 22 / pass 21 / fail 1\n\n\nThe reverted-code failure message shows the exact bug the fix is preventing: record.mjs followed the symlink and wrote through to the directory outside PLUGIN_DATA, so the sentinel file got replaced with a JSON envelope. The new test would have caught this if it had existed in the original PR.\n\nI also fixed two adjacent Windows-specific bugs in the same ensureContained rewrite that the original round-4 reply glossed over:\n\n1. realpathOf was returning the input path on ENOENT (treating "doesn't exist yet" as "realpath of self"). This silently defeated the realpath comparison on Windows when the target was a new file inside an existing directory. The fix lets ENOENT propagate and the caller handles the parent-realpath fallback.\n2. target.slice(parent.length + 1) is length-dependent. On Windows, mkdtemp returns a short 8.3 path (C:\\Users\\ADMINI~1\\...) while realpath returns the long form (C:\\Users\\Administrator\\...), and the slice produced a corrupted basename like tate.json. Switched to path.relative(parent, target) which is length-independent.\n\nBoth were caught while running the basic MAX_STATE_BYTES test on Windows during the round-4 work. The full POSIX verification above is the first time the realpath code has been exercised on actual Linux, and the round-trip is the first time the symlink escape has been confirmed end-to-end. No further changes needed for R4-2; the fix is verified locally on both platforms."}

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head 266068e closes the previous traversal, symlink-containment and schema-pinning blockers. Focused validation tests pass 34/34 and the full repository test run passes 284/284 locally.

One normative contract inconsistency remains: proposals/hooks-detailed-spec.md still says any non-empty $schema value is accepted, while scripts/lib/validation.mjs now correctly requires the exact https://minimax.io/schemas/mcode-hooks-v1.json URL. Please update the proposal text so the specification and validator define the same contract, then rerun the tests.

No Actions run exists for this head; [code]smith is SKIPPED.

Round-5 review on commit 266068e flagged one normative contract
inconsistency: proposals/hooks-detailed-spec.md line 320 said the
validator accepts any non-empty $schema string, while the validator
(scripts/lib/validation.mjs:275) and the same proposal (line 334-335)
require $schema to exactly equal the pinned URL. The two statements
defined different contracts; the validator code is the authoritative
one.

This commit removes the stale "non-empty string" bullet from the
"validator enforces" list. The exact-equals clause already lives
in the same list further down, so the authoritative contract is
now stated once and matches the validator assertion.

Validation
- node --test test/validation.test.mjs: 22/22 pass
  (unchanged from 266068e; 0 new tests, 0 modified tests)
- node --test (full suite): 127/128 pass. The single remaining
  fail is the pre-existing test/hosted-plugins.test.mjs:15
  Windows-only POSIX-path-regex bug; it fails identically before
  and after this commit and is unchanged by the spec edit.

Design compliance
- 1 file changed, 1 deletion(-). Only the contradicting bullet
  is removed; no rewording of neighbouring bullets, no renumbering.
- HOOK_SCHEMA constant value
  (https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json)
  is unchanged and still matches the URL cited in the proposal's
  example (line 225) and the exact-equals clause (line 334-335).
@antianqi

Copy link
Copy Markdown
Contributor Author

Round-5 review on $schema contract (1 deletion, no code/test changes)

@hetaoBackend Thanks for catching the normative inconsistency on the round-5 review. Pushed as commit 4f22672 on top of 266068e (single file, single deletion).

What changed

proposals/hooks-detailed-spec.md previously had two statements of the $schema contract in the same "validator enforces" list:

  • line 320: $schema` as a non-empty string
  • line 334-335: $schema` exactly equals `https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json`

The first bullet predated the 266068e R4-3 fix that pinned the URL; the second bullet was added (or restated) in the same commit. The validator at scripts/lib/validation.mjs:275 has been assert(value.$schema === HOOK_SCHEMA, ...) since 266068e, so the first bullet had been stale for one commit.

4f22672 removes the stale "non-empty string" bullet. The exact-equals clause a few lines lower is now the single source of truth for the $schema contract, and it matches the validator assertion byte-for-byte. The HOOK_SCHEMA constant value itself is unchanged.

Diff

- `hooks.json` declares `$schema` as a non-empty string.

One file changed, 1 deletion(-). No rewording of neighbouring bullets. No renumbering. The 21 remaining bullets in the "validator enforces" list are unchanged.

Validation

  • node --test test/validation.test.mjs: 22 / 22 pass (unchanged from 266068e; this commit touches no code or test, so 0 new tests, 0 modified tests, 0 failed tests).
  • node --test (full repository test suite on Windows): 127 / 128 pass. The single remaining fail is the pre-existing test/hosted-plugins.test.mjs:15 Windows-only POSIX-path-regex bug acknowledged in the original PR description; it fails identically on this commit and on 266068e and is unchanged by the spec edit.

Design compliance

  • The exact-equals clause (spec line 334-335), the example payload (spec line 225), the HOOK_SCHEMA constant (validation.mjs:11), and the validator assertion (validation.mjs:275) now all reference the same URL string: https://minimax.io/schemas/mcode-hooks/0.1.0/hooks.schema.json.
  • No reachability change: the proposal still explicitly states (line 346-348) that the validator pins the URL but does not fetch it; reachability remains a deployment-time concern.

Closes the round-5 review blocker on the $schema contract. Ready for another review pass.

antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 29, 2026
…le platform evidence

Round-5 review (hetaoBackend, 2026-08-28T08:22:25Z) on commit 38413d9
flagged one remaining blocker: executable platform evidence. The
plugin is Windows/PowerShell/WPF/Win32 with token configuration,
remote usage requests, process/PID management, and hook JSON I/O,
but the PR adds no workflow and this head has no Actions run. The
Node smoke is static and does not execute the PowerShell scripts.

This commit adds a new windows-latest Actions job at
`.github/workflows/mcode-island-windows.yml` that exercises the
four contract surfaces the round-5 review called for:

1. **Parse all `.ps1` files** (round-5 requirement #1). Static
   syntax check using
   `[System.Management.Automation.Language.Parser]::ParseFile`
   over the 27 `.ps1` files under `plugins/antianqi/mcode-island/`.
   A future change that introduces a PowerShell syntax error
   anywhere in the plugin (main script, hooks/scripts/*.ps1,
   set-token, notify-island, detector, ...) will fail this step.
   Verified locally: 27 / 27 parsed on commit 38413d9.

2. **Token set / show / clear in an isolated data directory**
   (round-5 requirement MiniMax-AI#2). `set-token.ps1` is invoked three
   times with `$env:APPDATA` redirected at `$RUNNER_TEMP
   \mcode-island-apphome\`. The detector's `$APPDATA\mcode-island
   \config.json` path is followed exactly; only the root is
   swapped. Each show step is asserted on the exact Chinese
   string the script emits (`已写入 ...`, `config.json
   planApiToken ...`, `已从 config.json 删除`, `token 未配置`).
   Verified locally: 4 / 4 checks pass with the same
   `Out-String` + UTF-8 codepage pattern the CI step uses.

3. **Mocked usage-API behavior** (round-5 requirement MiniMax-AI#3). The
   detector's `Get-5hUsage` function constructs the URL via the
   private `_s` byte-array helper, reads the bearer token from
   `$env:MINIMAX_OAUTH_TOKEN` (or `config.json planApiToken`),
   and calls `Invoke-RestMethod` against `api.minimaxi.com/v1/
   coding_plan/remains`. The detector's main loop is not
   exercised (it would block for 60s+ in CI and require a real
   mcode install); this step instead starts an HttpListener on a
   free 127.0.0.1 port in a `Start-Job` and sync-waits for one
   request. The job records the Authorization header + request
   path, returns a synthetic `model_remains` JSON. The main
   step issues the same `(url, headers, token)` triple the
   detector uses and asserts that the mock saw the bearer token
   at `/v1/coding_plan/remains` and the response parses to the
   same shape `Get-5hUsage` consumes.

4. **Hook stdin / stdout paths** (round-5 requirement MiniMax-AI#4). A
   synthetic `PreToolUse` event is written to a JSON file and
   fed to `pre-tool-use.ps1` via `Start-Process
   -RedirectStandardInput` (PowerShell 5.1 `$string | & .ps1`
   does NOT rewire the child process's stdin; only stdout / stderr
   cross the pipeline). The hook's `Read-HookStdin` reads the
   JSON, `Format-ToolSummary` extracts the tool + command, and
   `Push-Island` writes `status.json` to the isolated APPDATA.
   The step then reads back `status.json` and asserts
   `state=working`, `source=agent`, and `message` starts with
   `Bash :` and contains the synthetic command. Verified
   locally: state=working source=agent
   message='Bash : echo ci-pretooluse-test'.

Design compliance
- 1 new file: `.github/workflows/mcode-island-windows.yml` (no
  changes to existing code). Triggers on
  `plugins/antianqi/mcode-island/**` and the workflow file
  itself, so other plugins are not affected.
- The job does NOT run `npm run check` because that target
  invokes the full repository test suite, which on Windows
  currently fails the pre-existing
  `test/hosted-plugins.test.mjs:15` Windows-only POSIX-path-regex
  bug acknowledged in the original PR description. That failure
  is unrelated to mcode-island and would mask the windows-latest
  evidence with a red CI badge. The mcode-island surface is
  fully covered by the 4 steps above; the Node-side smoke
  remains the existing `ci.yml` ubuntu-latest job.
- The job does NOT open the WPF UI (no explorer.exe, no logon
  session) and does NOT run the `mcode-status-detect.ps1` main
  loop (which would block for 60s+ in CI and require a real
  mcode install). Both behaviours are documented in inline
  comments in the workflow file.
- The job does NOT call the real `api.minimaxi.com` endpoint. The
  mock listener is on 127.0.0.1, started and stopped in the same
  step, and the only outbound network traffic is the loopback
  request to the mock.
- `[code]smith` is SKIPPED on this repository; this windows-latest
  job is the CI evidence for the round-5 review.

Negative-injection contracts
- Step 1 fails if any `.ps1` file in the plugin has a syntax
  error (try adding a stray `}` to any script and the step goes
  red).
- Step 2 fails if `set-token.ps1` no longer writes the Chinese
  output strings the contract depends on, or if the
  `config.json` read/write is broken.
- Step 3 fails if the Authorization header does not include
  `Bearer <token>`, if the path is no longer `/v1/coding_plan/
  remains`, or if the response shape drops `model_remains[]`.
- Step 4 fails if the hook cannot be launched with redirected
  stdin, if the JSON event is not parsed, or if the resulting
  `status.json` does not have `state=working source=agent
  message='Bash : ...'`.

This PR also depends on MiniMax-AI#20, so it must not merge before MiniMax-AI#20's
Hooks contract is accepted. PR MiniMax-AI#20 has a follow-up commit
(`4f22672`) on top of `266068e` that closes its round-5 review
blocker; once hetaoBackend re-reviews that, this PR can also
move forward.
antianqi added a commit to antianqi/MiniMax-Code-Plugins-1 that referenced this pull request Aug 29, 2026
… step 3 (yaml fix)

The v1 commit (6a9e7c6) put a PowerShell here-doc (`@'...'@`) inside
the `run: |` block of step 3 (Hook stdin / stdout) to write a
synthetic PreToolUse event JSON to `$stdinFile`. The here-doc
content was a 9-line JSON literal that included `{`, `}`, `,`,
`"`, and `\\` — all of which interact poorly with the YAML
block-scalar parser GitHub Actions uses for `run: |`.

A `js-yaml` parse of the v1 file fails with:

  can not read a block mapping entry; a multiline key may not be
  an implicit key (187:2)

at the closing `'@ | Out-File ...` line. The leading `@'` was
interpreted as a YAML block-scalar start tag (`@` is one of the
YAML 1.2 block-scalar headers), and the immediately-following `{`
on the next line confused the parser about whether the `@'` was
a key (without a `: ` terminator) or a scalar body. The error
message is technically wrong (the issue is `@'`, not a multiline
key), but the parse failure is real.

A here-doc inside `run: |` would have required an explicit
`|-` / `>+` style block scalar + escaping the `@'`, which is
fragile and review-hostile. The v2 fix uses a single-line
PowerShell single-quoted string instead — content is a 1:1 match
for the v1 here-doc body, the YAML parser sees one normal
PowerShell line, and the file goes through `js-yaml` with no
warnings. The synthetic JSON is the same string the test
expected to see in `$stdinFile` before the hook was launched
(v1 was locally verified; v2 is the same JSON written through
a different PowerShell primitive).

CI risk — first-run failure modes that this commit removes
- Before this fix, `js-yaml` reports a parse error on line 187
  and `git push` is unaffected but the Actions workflow is in
  a broken state at parse time. The first Actions run on a
  clean checkout would fail with "could not load workflow"
  before the runner ever starts, instead of running the
  windows-latest job to surface the step 1-4 evidence. This
  commit makes the workflow parseable.
- The `Start-Process` + `-RedirectStandardInput` invocation
  is unchanged. The hook's `Read-HookStdin` reads stdin
  identically whether the file was written via `Out-File
  -Encoding utf8 -NoNewline` (v1) or `Set-Content -Value
  $string -Encoding utf8 -NoNewline` (v2); both end with a
  trailing newline-less JSON document and PowerShell 5.1 +
  PowerShell 7 write UTF-8 without BOM by default in this
  context. Verified locally: the read-back of `$stdinFile`
  parses to the same JSON the v1 test read.

Validation
- `js-yaml` parse of `.github/workflows/mcode-island-windows.yml`:
  clean, no warnings. `run: |` block parses to a string, the
  step 3 step body is the expected `$hook = ...` line, the
  new `$stdinJson` line, and the `Set-Content` line.
- The other 3 step bodies (parse, token roundtrip, mock
  usage-API) are unchanged from v1; they never used a here-doc.

Design compliance
- 1 file changed: `.github/workflows/mcode-island-windows.yml`
  (+12 / -10 lines). No code or Skills change. No `npm`
  dependencies added, removed, or upgraded. The fix is
  pure YAML / PowerShell surface compatibility.
- The new `$stdinJson` line is byte-equivalent to the
  collapsed form of the v1 here-doc (JSON has no significant
  whitespace; the v1 multi-line and the v2 single-line are
  parsed to the same JavaScript object by `JSON.parse` and the
  same PowerShell `ConvertFrom-Json`).

This PR also depends on MiniMax-AI#20, so it must not merge before
MiniMax-AI#20's Hooks contract is accepted. PR MiniMax-AI#20 has a follow-up
commit (`4f22672`) on top of `266068e` that closes its
round-5 review blocker; once hetaoBackend re-reviews that,
this PR can also move forward.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants